04 — 变换物体

Three.js Journey · Bruno Simon · 难度:简单

04 — 变换物体 课程海报

介绍

既然一切都已就绪,现在我们可以探索 Three.js 的各种功能了。

在让场景动起来之前,我们需要知道如何在场景中变换物体。其实我们已经对相机做过一次了——用 camera.position.z = 3 把它向后移。

场景中变换物体有 4 个属性:

所有继承自 Object3D 类的类都拥有这些属性,比如 PerspectiveCamera 或 Mesh,以及我们还没涉及到的其他类。

你可以在 Three.js 文档的顶部看到每个类都继承了哪些类。

这些属性会编译成我们所说的 matrix(矩阵)。Matrix 在内部被 Three.js、WebGL 和 GPU 用来变换物体。幸运的是,你不需要自己处理 matrix,只需修改前面提到的那些属性就够了。

准备工作

在 starter 文件中,我们只保留上一课结束时的那个项目,一个立方体位于视图中央。

移动物体

position 拥有 3 个基本属性:x、y 和 z。这三个是在 3D 空间中定位物体所必需的轴。

每个轴的方向完全是人为约定的,可能因环境而异。在 Three.js 中,我们通常认为 y 轴向上,z 轴向后,x 轴向右。

至于 1 个单位代表多少,就由你决定了。1 可以是 1 厘米、1 米,甚至是 1 公里。我建议你根据想构建的东西来调整单位。如果你要建一栋房子,你大概应该把 1 个单位想成 1 米。

你可以随意摆弄 mesh 的 position 属性,试着预测立方体会跑到哪里(尽量让它保持在屏幕内)。

请务必在调用 render(...) 方法之前做这些,否则你会在移动 mesh 之前就渲染它了。

mesh.position.x = 0.7
mesh.position.y = - 0.6
mesh.position.z = 1

position 属性并不是任意对象。它是 Vector3 类的一个实例。这个类除了有 x、y、z 属性外,还有很多有用的方法。

你可以获取一个向量的长度:

console.log(mesh.position.length())

你可以获取到另一个 Vector3 的距离(确保在创建相机之后再使用这段代码):

console.log(mesh.position.distanceTo(camera.position))

你可以把它的值归一化(意思是把向量的长度缩小到 1 个单位,但保持方向不变):

console.log(mesh.position.normalize())

要修改这些值,除了单独改 x、y 和 z,你也可以用 set(...) 方法:

mesh.position.set(0.7, - 0.6, 1)

坐标轴辅助器

在我们继续之前你要知道,在空间里定位物体可能真的很有挑战性。弄清楚每个轴朝向哪里很复杂,尤其当我们开始移动相机时。

一个不错的解决方案是使用 Three.js 的 AxesHelper。

AxesHelper 会显示 3 条线,分别对应 x、y、z 轴,每条线都从场景中心出发,朝对应方向延伸。

要创建 AxesHelper,实例化它,然后在实例化 scene 之后立即把它添加到 scene 中。唯一参数可以指定线条的长度。我们使用 2:

/**
 * Axes Helper
 */
const axesHelper = new THREE.AxesHelper(2)
scene.add(axesHelper)

你应该能看到一条绿色和一条红色的线。

绿色线对应 y 轴。红色线对应 x 轴,还有一条蓝色线对应 z 轴,但我们现在看不到它,因为它和相机完全对齐。

接下来的课程里我们不会用这个 AxesHelper,但如果你需要视觉参考,随时可以加上它。

缩放物体

scale 也是一个 Vector3。默认情况下,x、y、z 都等于 1,意思是物体没有应用任何缩放。如果你设成 0.5,物体在该轴上会变成原尺寸的一半;如果你设成 2,它在该轴上会变成原来的两倍大。

如果你修改这些值,物体会开始相应地缩放。注释掉 position,加上这些缩放:

mesh.scale.x = 2
mesh.scale.y = 0.25
mesh.scale.z = 0.5

显然,我们看不出 z 轴的缩放效果,因为我们的 Mesh 正对着相机。

虽然你可以用负值,但它可能会在以后引发 bug,因为各轴的方向会不符合逻辑。尽量别这么做。

因为它是一个 Vector3,所以我们可以使用前面提到的所有方法。

旋转物体

旋转比 position 和 scale 稍微麻烦一些。处理旋转有两种方式。

你可以用显而易见的 rotation 属性,但也可以用不那么直观的 quaternion 属性。Three.js 两者都支持,更新其中一个会自动更新另一个。只是看你更喜欢哪个。

Rotation

rotation 属性也有 x、y、z 属性,但它不是 Vector3,而是 Euler。当你修改一个 Euler 的 x、y、z 属性时,你可以想象穿了一根沿着该轴方向的棍子,穿过物体中心,然后让物体绕这根棍子旋转。

这些轴的值以弧度(radians)为单位。如果你想旋转半圈,你得写类似 3.14159... 的值。你大概认出来了,这个数就是 π。在原生 JavaScript 中,你可以用 Math.PI 得到 π 的近似值。

注释掉 scale,并在 x 和 y 两轴上各四分之一圈旋转:

mesh.rotation.x = Math.PI * 0.25
mesh.rotation.y = Math.PI * 0.25

容易吧?是的,但当你把这些旋转组合起来时,可能会出现奇怪的结果。为什么?因为你在旋转 x 轴时,同时也改变了其他轴的方向。旋转按以下顺序应用:x、然后是 y、最后是 z。这可能会导致奇怪的行为,比如在某一轴不再有任何作用时出现 gimbal lock(万向节锁),这一切都是因为前面轴的旋转造成的。

我们可以用 reorder(...) 方法改变这个顺序:object.rotation.reorder('YXZ')

虽然 Euler 更容易理解,但这个顺序问题会带来麻烦。这正是大多数引擎和 3D 软件采用另一种名为 Quaternion 的解决方案的原因。

Quaternion

quaternion 属性同样是表达旋转,但它的数学方式更严谨,解决了顺序问题。

我们不会在本课深入讲解 quaternion 的运作原理,但要记住:当你修改 rotation 时,quaternion 会同步更新。这意味着你可以随意使用两者中的任何一个。

看这里!

Object3D 实例有一个非常棒的方法叫 lookAt(...),可以让你让某个物体看向某个东西。物体将自动把它的 -z 轴旋转,指向你提供的目标。不需要复杂的数学。

你可以用它让相机转向某个物体,让大炮对准敌人,或者让角色的眼睛看向物体。

参数就是目标,必须是一个 Vector3。你可以创建一个来试试:

camera.lookAt(new THREE.Vector3(0, - 1, 0))

立方体看起来变高了,但其实是因为相机正看向立方体下方的位置。

我们也可以用任何现成的 Vector3,比如 mesh 的 position,但那会导致相机回到默认位置,因为我们的 mesh 位于 scene 的中心。

camera.lookAt(mesh.position)

组合变换

你可以按任意顺序组合 position、rotation(或 quaternion)以及 scale。结果都是一样的。它就等价于物体的状态。

让我们把之前试过的所有变换组合起来:

mesh.position.x = 0.7
mesh.position.y = - 0.6
mesh.position.z = 1
mesh.scale.x = 2
mesh.scale.y = 0.25
mesh.scale.z = 0.5
mesh.rotation.x = Math.PI * 0.25
mesh.rotation.y = Math.PI * 0.25

场景图

到某个阶段,你可能会想把东西分组。假设你在建一栋房子,有墙、门、窗、屋顶、灌木丛等等。

当你觉得建好了,却发现房子太小,你得重新缩放每个物体并更新它们的位置。

一个好办法是把所有这些物体都放进一个容器里,然后缩放这个容器。

你可以用 Group 类来做这件事。

实例化一个 Group 并把它添加到场景中。现在,当你想创建新物体时,你可以用 add(...) 方法把它添加到刚创建的 Group 里,而不是直接添加到场景。

因为 Group 类继承自 Object3D 类,所以它可以访问前面提到的属性和方法,比如 position、scale、rotation、quaternion 和 lookAt。

注释掉 lookAt(...) 调用,然后不创建之前那个立方体,而是创建 3 个立方体并添加到同一个 Group 里。然后再对 group 应用变换:

/**
 * Objects
 */
const group = new THREE.Group()
group.scale.y = 2
group.rotation.y = 0.2
scene.add(group)

const cube1 = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshBasicMaterial({ color: 0xff0000 })
)
cube1.position.x = - 1.5
group.add(cube1)

const cube2 = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshBasicMaterial({ color: 0xff0000 })
)
cube2.position.x = 0
group.add(cube2)

const cube3 = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshBasicMaterial({ color: 0xff0000 })
)
cube3.position.x = 1.5
group.add(cube3)

顺序其实无所谓,只要是合法的 JavaScript 就行。

既然我们已经知道如何变换物体,是时候创建一些动画了。